<?php
declare(strict_types=1);

namespace WebApp\lib\db\mysql;

use ErrorException;
use PDO;
use PDOException;
use Throwable;
use WebApp\lib\db\IDriver;
use WebApp\lib\util\LogHelper;
use WebApp\lib\util\Util;

/**
 * Class NormalTable
 * @package WebApp\lib\db
 */
class NormalDriver extends MysqlDriver implements IDriver
{
    public function __construct(IMysqlConfig $config)
    {
        parent::__construct($config);
    }

    /**
     * @param $arr
     * @return mixed
     */
    public function transaction($arr)
    {
        return $func = function (Pdo $pdo) use (&$arr) {

            $pdo->beginTransaction();
            try {
                foreach ($arr as $res) {
                    if (gettype($res) !== 'object') {
                        throw new ErrorException();
                    }
                    $res = (bool)$res();
                    if (!$res) {
                        throw new ErrorException();
                    }
                }
                $pdo->commit();
                return true;
            } catch (PDOException $e) {
                $pdo->rollBack();
                throw $e;
            } catch (ErrorException $e) {
                $pdo->rollBack();
                throw $e;
            }
        };
    }

    /**
     * @param string $sql
     * @param array $data
     * @return mixed
     */
    public function getObjArrBySql(string $sql, $data=[])
    {
        return function (Pdo $pdo) use($sql) {
            $this->setLastSql($sql);

            $m_stmt = $pdo->prepare($sql);
            if (!$m_stmt) {
                Util::logD($sql);
                Util::logObj($pdo->errorInfo());
                throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
            }
            $m_stmt->execute();
            $res = $m_stmt->fetchAll(PDO::FETCH_ASSOC);
            $m_stmt->closeCursor();
            return $res;
        };
    }

    public function getObjBySql(string $sql, $data=[])
    {
        return function (Pdo $pdo) use($sql) {

            $this->setLastSql($sql);
            $m_stmt = $pdo->prepare($sql);
            if (!$m_stmt) {
                throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
            }
            $m_stmt->execute();
            $res = $m_stmt->fetch(PDO::FETCH_ASSOC);
            if (in_array($res, [false, null])) {
                Util::logObj($res);
                LogHelper::logD($sql);
                LogHelper::logD($m_stmt->errorInfo());
            }

            $m_stmt->closeCursor();
            return $res;
        };
    }

    public function updateObj($db_name, $data = [], $where = [])
    {
        return function (Pdo $pdo) use (&$db_name, &$data, &$where) {

            $str = $this->makeWhere($where);
            $arr = self::makeSetData($data);
            $set = $arr[0];
            $data = $arr[1];
            $sql = "UPDATE $db_name $set $str";
            return $this->executeWithData($pdo, $sql, [$data, $where]);
        };
    }

    public function removeObj($db_name, $where = [])
    {
        return function (Pdo $pdo) use (&$db_name, &$where) {

            $str = $this->makeWhere($where);
            $sql = /** @lang text */
                "DELETE FROM $db_name $str";
            return $this->executeWithData($pdo, $sql, $where);
        };
    }

    public function replaceObj($db_name, $data = [])
    {
        return function (Pdo $pdo) use (&$db_name, &$data) {

            $arr = self::makeSetData($data);
            $set = $arr[0];
            $data = $arr[1];
            return $this->executeWithData($pdo, /** @lang text */
                "REPLACE INTO $db_name $set", $data);
        };
    }

    public function addObj($db_name, $data = [])
    {
        return function (Pdo $pdo) use (&$db_name, &$data) {

            $arr = self::makeSetData($data);
            $str = $arr[0];
            $data = $arr[1];
            $sql = /** @lang text */
                "INSERT INTO $db_name $str";

            $res = $this->executeWithData($pdo, $sql, $data);
            if ($res) {
                $last_id = (int)$pdo->lastInsertId();
                return $last_id;
            } else {
                Util::logObj($pdo->errorCode());
                Util::logObj($pdo->errorInfo());
                $this->setLastErrInfo(json_encode($pdo->errorInfo()));
                $this->setLastErrCode($pdo->errorCode());
                return $res;
            }
        };
    }

    public function getObjCount($db_name, $where = [])
    {
        return  function (Pdo $pdo) use (&$db_name, &$where) {

            $str = $this->makeWhere($where);
            $_sql = /** @lang text */
                "SELECT count(*) FROM $db_name $str";
            $m_stmt = $pdo->prepare($_sql);
            if (!$m_stmt) {
                throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
            }
            $this->bindParams($m_stmt, $where);
            $m_stmt->execute();
            $res = $m_stmt->fetch(PDO::FETCH_ASSOC);
            $m_stmt->closeCursor();
            if (!is_array($res) || !$res) {
                return 0;
            } else {
                return array_values($res)[0];
            }
        };
    }

    public function getObjArr($db_name, $need = [], $where = [], $group = '', $sort = '', $limit = '')
    {
        return function (Pdo $pdo) use (&$db_name, &$need, &$where, &$group, &$sort, &$limit) {

            $sql = /** @lang text */
                "SELECT {$this->makeNeed($need)} FROM $db_name {$this->makeWhere($where)} {$this->makeGroup($group)} {$this->makeOrder($sort)} {$this->makeLimit($limit)}";
            $this->setLastSql($sql);
            $m_stmt = $pdo->prepare($sql);
            if (!$m_stmt) throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
            if ($where) $this->bindParams($m_stmt, $where);
            $m_stmt->execute();
            $res = $m_stmt->fetchAll(PDO::FETCH_ASSOC);
            $m_stmt->closeCursor();
            $this->setLastParams([
                "where"=> $where,
                "need"=> $need,
                "group"=> $group,
                "sort"=> $sort,
                "limit"=> $limit,
            ]);

            if ($res === false && $pdo->errorCode() != "00000") {
                $this->setLastErrInfo(json_encode($pdo->errorInfo()));
                $this->setLastErrCode($pdo->errorCode());
                LogHelper::logE("sql=$sql;last_err=" . $this->getLastErrInfo());
            }
            if (!is_array($res) || !$res) {
                return [];
            } else {
                return $res;
            }
        };
    }

    public function getObjArrV1(&$rows, string $db_name, $select, array $where = [], string $group = '', $sort = '', array $limit = []): callable
    {
        return function (Pdo $pdo) use (&$rows, &$db_name, &$select, &$where, &$group, &$sort, &$limit): void {

            $sql = /** @lang text */
                "SELECT {$this->makeNeedV1($select)} FROM $db_name {$this->makeWhere($where)} {$this->makeGroup($group)} {$this->makeOrder($sort)} {$this->makeLimitV1($limit)}";
            $m_stmt = $pdo->prepare($sql);
            if (!$m_stmt) throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
            if ($where) $this->bindParams($m_stmt, $where);
            $m_stmt->execute();
            $rows = $m_stmt->fetchAll(PDO::FETCH_ASSOC);
            $m_stmt->closeCursor();
            $this->setLastSql($sql);
            $this->setLastParams([
                "where"=> $where,
                "need"=> $select,
                "group"=> $group,
                "sort"=> $sort,
                "limit"=> $limit,
            ]);

            if ($rows === false && $pdo->errorCode() != "00000") {
                $this->setLastErrInfo(json_encode($pdo->errorInfo()));
                $this->setLastErrCode($pdo->errorCode());
                LogHelper::logE("sql=$sql;last_err=" . $this->getLastErrInfo());
            }
            if (!is_array($rows) || !$rows) {
                $rows = [];
            }
        };
    }

    public function getObj($db_name, $need = [], $where = [], $group = '', $sort = ''): callable
    {
        return function (Pdo $pdo) use (&$db_name, &$need, &$where, &$group, &$sort) {
            $_sql = /** @lang text */
                "SELECT {$this->makeNeed($need)} FROM $db_name 
                {$this->makeWhere($where)} {$this->makeGroup($group)} {$this->makeOrder($sort)} limit 1";
            $row = $this->getObj_step($pdo, $_sql, $where, $need, $group, $sort);
            if (!is_array($row) || !$row) {
                return [];
            } else {
                return $row;
            }
        };
    }

    private function getObj_step(PDO $pdo, string $_sql, array $where, $need, $group, $sort) {
        $this->setLastSql($_sql);
        $this->setLastParams([
            "where"=> $where,
            "need"=> $need,
            "group"=> $group,
            "sort"=> $sort,
        ]);
        $m_stmt = $pdo->prepare($_sql);
        if (!$m_stmt) throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
        if ($where) $this->bindParams($m_stmt, $where);
        $m_stmt->execute();
        $row = $m_stmt->fetch(PDO::FETCH_ASSOC);
        $m_stmt->closeCursor();
        if ($row === false && $pdo->errorCode() != "00000") {
            $this->setLastErrInfo(json_encode($pdo->errorInfo()));
            $this->setLastErrCode($pdo->errorCode());
        }
        return $row;
    }

    public function getObjV1(&$row, $db_name, $select, array $where = [], $group = '', $sort = ''): callable
    {
        return function (Pdo $pdo) use (&$row, &$db_name, &$select, &$where, &$group, &$sort): void {
            $_sql = /** @lang text */
                "SELECT {$this->makeNeedV1($select)} FROM $db_name 
                {$this->makeWhere($where)} {$this->makeGroup($group)} {$this->makeOrder($sort)} limit 1";
            $row = $this->getObj_step($pdo, $_sql, $where, $select, $group, $sort);
            if (!is_array($row) || !$row) $row = [];
        };
    }

    /**
     * @param string $_sql
     * @return mixed
     */
    public function execute(string $_sql)
    {
        return function (Pdo $pdo) use($_sql) {

            $m_stmt = $pdo->prepare($_sql);
            if (!$m_stmt) {
                throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
            }
            $res = $m_stmt->execute();
            $this->setLastId($pdo->lastInsertId());
            $m_stmt->closeCursor();
            if ($res === false && $pdo->errorCode() != "00000") {
                $this->setLastErrInfo(json_encode($pdo->errorInfo()));
                $this->setLastErrCode($pdo->errorCode());
                LogHelper::logE("sql=$_sql;json=" . json_encode([
                    'code'=> $pdo->errorCode(),
                    'info'=> $pdo->errorInfo()
                ]));
            }
            $this->setLastSql($_sql);
            return $res;
        };
    }

    /**
     * @param PDO $pdo
     * @param $_sql
     * @param $data
     * @return bool
     */
    private function executeWithData(PDO $pdo, $_sql, $data)
    {
        $this->setLastSql($_sql);
        $m_stmt = $pdo->prepare($_sql);
        if (!$m_stmt) {
            throw new PDOException(json_encode($pdo->errorInfo()), $pdo->errorInfo()[1]);
        }
        $this->bindParams($m_stmt, $data);

        $ret = $m_stmt->execute();
        $m_stmt->closeCursor();
        if (!$ret) {
            Util::logObj([
                $pdo->errorCode(),
                $this->getLastErrInfo(),
                $_sql,
                $data
            ]);
        }
        if ($ret === false && $pdo->errorCode() != "00000") {
            $this->setLastErrCode($pdo->errorCode());
            $this->setLastErrInfo(json_encode($pdo->errorInfo()));
            Util::logObj([
                $pdo->errorCode(),
                $this->getLastErrInfo()
            ]);
            LogHelper::logE("sql=$_sql;last_err=" . $this->getLastErrInfo());
        }

        $this->setLastParams([
            "data"=> $data,
        ]);

        return $ret;
    }

    public function getLastSql()
    {
        return $this->last_sql;
    }

    public function getLastId()
    {
        return $this->last_id;
    }

    public function getLastErrInfo(): string
    {
        return $this->last_err_info;
    }

    public function getLastErrCode(): string
    {
        return $this->last_err_code;
    }

    public function setLastSql($sql)
    {
        $this->last_sql = $sql;
    }

    public function setLastId($id)
    {
        $this->last_id = $id;
    }

    public function setLastErrInfo(string $info)
    {
        $this->last_err_info = $info;
    }

    public function setLastErrCode($code)
    {
        $this->last_err_code = $code;
    }

    public function getLastParams(): array
    {
        return $this->last_params;
    }

    public function setLastParams($params)
    {
        $this->last_params = $params;
    }

    public function getLastErr()
    {
        // TODO: Implement getLastErr() method.
    }

    /**
     * @return PDO
     */
    public function getCon()
    {
        $dbms='mysql';     //数据库类型
        $host=$this->config->getHost(); //数据库主机名
        $dbName=$this->config->getDbName();    //使用的数据库
        $user=$this->config->getUser();      //数据库连接用户名
        $pass=$this->config->getPwd();          //对应的密码
        $dsn="$dbms:host=$host;dbname=$dbName";
        $conn = new PDO($dsn, $user, $pass, [
            PDO::ATTR_PERSISTENT => true,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES=> false
        ]);
//        tracker("ATTR_STRINGIFY_FETCHES", true);
        return $conn;
    }

    /**
     * @param PDO $conn
     * @param Throwable $e
     */
    public function catchErr(&$conn, Throwable $e)
    {
        $err_msg = $e->getMessage();
        if (Util::hasString($err_msg, "2002")) {
            unset($conn);
        }
    }

    public function getTryCount(): int
    {
        return $this->config->getTryCount();
    }
}



